/*
 * A module that mostly implements the "inert" specification, inspired by https://github.com/WICG/inert, but lighter.
 *
 * An element is marked inert by adding an "inert" attribute:
 *
 *     <div inert>
 *
 * A mutation observer watches for this and configures the element to make the entire subtree non-interactive: invisible
 * to pointing devices, screen readers, and keyboard navigation.
 *
 * One key aspect is to make child elements unfocusable and retain enough information to revert them back to their
 * previous focusable state when their tree becomes not-inert. There are four scenarios:
 *
 * focusable                 unfocusable
 * ----------------------    ---------------------------------------------------------------------
 * 1. <span>                 <span data-inert-tabindex>
 * 2. <span tabindex=x>      <span data-inert-tabindex=x>
 * 3. <button>               <button data-inert-tabindex tabindex=-1>  (and button.focus = ()=>{})
 * 4. <button tabindex=x>    <button data-inert-tabindex tabindex=x>   (and button.focus = ()=>{})
 *
 * (1 and 2 apply to any natively non-interactive element)
 * (3 and 4 apply to any natively interactive element)
 *
 * The "data-inert-tabindex" attribute's value is used to store the original tabindex. No value signifies that the
 * element did not have a explicit tabindex specified.
 *
 * The tabindex for an inert element can be changed by modifying the data-inert-tabindex attribute. The setTabindex
 * utility function chooses the appropriate attribute to modify depending on the inertness state of the element.
 *
 * Note: This implementation does _not_ handle the addition of nodes to an inert tree.
 * Note: Inert root elements are also made unfocusable.
 *
 * For a complete implementation of inert, CSS styles do most of the work and should be defined in the document:
 *
 *   [inert], [inert] * {
 *       cursor: default !important;
 *       pointer-events: none !important;
 *       user-select: none !important;
 *       -webkit-user-select: none !important;
 *       -moz-user-select: none !important; /!* needed? *!/
 *   }
 */

// import * as d3 from "../lib/d3.js";
import * as d3 from "d3";
import {isInert} from "./util.js";
import {focusableElements} from "./focusable.js";

const unfocusedElements = d3.selectorAll("[data-inert-tabindex]");

/**
 * @param elements d3 selection
 */
function makeUnfocusable(elements) {
    elements.each(function() {
        if (this.hasAttribute("data-inert-tabindex")) {
            return;
        }
        this.setAttribute("data-inert-tabindex", this.hasAttribute("tabindex") ? this.getAttribute("tabindex") : "");
        this.removeAttribute("tabindex");
        if (this.tabIndex >= 0) {
            // If this element still has a tabbable index after removing the tabindex attribute, then it is a
            // natively focusable element. Forcefully make this element unfocusable and untabbable.
            this.tabIndex = -1;
            this.focus = () => {};
        }
    });
}

/**
 * @param elements d3 selection
 */
function makeFocusable(elements) {
    elements.each(function() {
        if (isInert(this)) {
            return;
        }
        if (this.hasAttribute("data-inert-tabindex")) {
            const inertTabindex = this.getAttribute("data-inert-tabindex") ?? "";
            this.removeAttribute("data-inert-tabindex");
            if (inertTabindex === "") {
                this.removeAttribute("tabindex");
            } else {
                this.tabIndex = inertTabindex;
            }
            delete this.focus;
        }
    });
}

/**
 * Sets the "tabindex" attribute on the specified elements, taking into account whether each element is inert or has
 * an inert ancestor (in which case, the desired tabindex is stored for use when the tree becomes not inert).
 *
 * @param elements d3 selection
 * @param {Function} tabindexCallback a d3-style attribute callback that returns the tabindex to set on each element.
 */
export function setTabindex(elements, tabindexCallback) {
    elements.each(function() {
        const tabindex = tabindexCallback.apply(this, arguments);
        this.setAttribute(isInert(this) ? "data-inert-tabindex" : "tabindex", tabindex);
    });
}

/**
 * Make all subtrees rooted by the specified containers inert.
 * @param containers d3 selection
 */
function makeInert(containers) {
    containers
        .attr("aria-hidden", true)
        .call(makeUnfocusable)
      .selectAll(focusableElements)
        .call(makeUnfocusable);
}

/**
 * Make all subtrees rooted by the specified containers not inert.
 * @param containers d3 selection
 */
function makeAlive(containers) {
    containers
        .attr("aria-hidden", null)
        .call(makeFocusable)
      .selectAll(unfocusedElements)
        .call(makeFocusable);
}

/**
 * Use a mutation observer to observe the entire document for the creation of "inert" attributes on elements,
 * which triggers the logic to mark their subtrees as unfocusable. Similar for the removal of attributes.
 */
void function initialize() {
    const observer = new MutationObserver(records => {
        const targets = records.map(record => record.target);
        d3.selectAll(targets.filter(e => e.hasAttribute("inert"))).call(makeInert);
        d3.selectAll(targets.filter(e => !e.hasAttribute("inert"))).call(makeAlive);
    });

    const root = d3.select(":root");
    observer.observe(root.node(), {attributes: true, attributeFilter: ["inert"], subtree: true});
    root.selectAll("[inert]").call(makeInert);  // Process any pre-existing inert elements.
}();
